﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Xml.Serialization;

namespace OrzLib
{
    [Serializable]
    public class OrzEntity<T> where T : new()
    {
        private static DirectoryInfo _defaultDirectory = new DirectoryInfo(typeof(T).Name);
        
        private static int _xml0Base64Str1Bytes2 = 0;

        public static DirectoryInfo DefaultDirectory
        {
            get { return _defaultDirectory; }
            set { _defaultDirectory = value; }
        }

        public static int XML0_Base64Str1_Bytes2
        {
            get { return _xml0Base64Str1Bytes2; }
            set { _xml0Base64Str1Bytes2 = value; }
        }

        public Guid UnqiueId { get; set; }

        public OrzEntity()
        {
            UnqiueId = Guid.NewGuid();
        }

        public OrzEntity(Guid unqiueKey)
        {
            UnqiueId = unqiueKey;
            Update();
        }

        public void Delete()
        {
            var fileId = new FileInfo(Path.Combine(_defaultDirectory.FullName, UnqiueId.ToString()));
            fileId.Delete();
        }

        public static void Delete(Guid unqiueId)
        {
            var fileId = new FileInfo(Path.Combine(_defaultDirectory.FullName, unqiueId.ToString()));
            fileId.Delete();
        }

        public bool Exists()
        {
            var fileId = new FileInfo(Path.Combine(_defaultDirectory.FullName, UnqiueId.ToString()));
            return fileId.Exists;
        }

        public static bool Exists(Guid unqiueId)
        {
            var fileId = new FileInfo(Path.Combine(_defaultDirectory.FullName, unqiueId.ToString()));
            return fileId.Exists;
        }

        public void Update()
        {
            UpdateBreathFirst(this);
        }

        public static T Update(Guid unqiueId)
        {
            T t = new T();
            t.GetType().GetProperty("UnqiueId").SetValue(t, unqiueId, null);
            UpdateBreathFirst(t);
            return t;
        }

        public void Save()
        {
            SaveRecursive(this);
        }

        public static void Save(T t)
        {
            SaveRecursive(t);
        }

        public static List<T> ListEntities()
        {
            var resultList = new List<T>();
            var dir = _defaultDirectory;

            foreach (var fileId in dir.GetFiles())
            {
                T t = new T();
                t.GetType().GetProperty("UnqiueId").SetValue(t, new Guid(fileId.Name), null);
                t.GetType().GetMethod("Update", new Type[] { }).Invoke(t, null);
                resultList.Add(t);
            }
            return resultList;
        }

        public static List<Guid> ListUnqiueIds()
        {
            var resultList = new List<Guid>();
            var dir = _defaultDirectory;
            foreach (var fileId in dir.GetFiles())
            {
                resultList.Add(new Guid(fileId.Name));
            }

            return resultList;

        }

        public static void CleanDirectory()
        {
            var dir = _defaultDirectory;
            foreach (var fileId in dir.GetFiles())
            {
                fileId.Delete();
            }
        }

        protected static void UpdateBreathFirst(object obj)
        {
            var val = obj.GetType().GetMethod("Deserialize", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic).Invoke(obj, null);
            ReflectionValueAssign(obj, val);
            Queue<object> _rootQueue = new Queue<object>();
            EnqueueChildren(obj, _rootQueue);
            while (_rootQueue.Count > 0)
            {
                var next = _rootQueue.Dequeue();
                EnqueueChildren(next, _rootQueue);

                var newobj = next.GetType().GetMethod("Deserialize", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic).Invoke(next, null);
                ReflectionValueAssign(next, newobj);
            }
        }

        protected static void EnqueueChildren(object obj, Queue<object> _rootQueue)
        {
            if (ReflectionBaseTypeCompare(obj.GetType(), typeof(OrzEntity<>)))
            {
                foreach (var propertyInfo in obj.GetType().GetProperties())
                {
                    if (ReflectionBaseTypeCompare(propertyInfo.PropertyType, typeof(OrzEntity<>)))
                    {
                        var item = propertyInfo.GetValue(obj, null);
                        if (item != null)
                        {
                            _rootQueue.Enqueue(item);
                        }
                    }
                    else if (ReflectionBaseTypeCompare(propertyInfo.PropertyType, typeof(List<>)))
                    {
                        var list = (IList)propertyInfo.GetValue(obj, null);
                        if (list != null)
                        {
                            foreach (object item in list)
                            {
                                _rootQueue.Enqueue(item);
                            }

                        }

                    }
                }
            }
        }

        protected static void SaveRecursive(object obj)
        {
            //if base type is OrzEntity, then we treat it as entity
            if (ReflectionBaseTypeCompare(obj.GetType(), typeof(OrzEntity<>)))
            {
                foreach (var propertyInfo in obj.GetType().GetProperties())
                {
                    if (ReflectionBaseTypeCompare(propertyInfo.PropertyType, typeof(OrzEntity<>)))
                    {
                        var item = propertyInfo.GetValue(obj, null);
                        if (item != null)
                        {
                            SaveRecursive(item);
                        }
                    }
                    else if (ReflectionBaseTypeCompare(propertyInfo.PropertyType, typeof(List<>)))
                    {
                        var list = (IList)propertyInfo.GetValue(obj, null);
                        if (list != null)
                        {
                            foreach (object item in list)
                            {
                                SaveRecursive(item);
                            }
                        }
                    }
                }

                obj.GetType().GetMethod("Serialize", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic).Invoke(obj, null);
            }

        }

        protected T Deserialize()
        {
            var fileId = new FileInfo(Path.Combine(_defaultDirectory.FullName, UnqiueId.ToString()));
            return DeserializeFromFile(this, fileId, XML0_Base64Str1_Bytes2);
        }

        protected void Serialize()
        {
            var fileId = new FileInfo(Path.Combine(_defaultDirectory.FullName, UnqiueId.ToString()));
            SerializeToFile(this, fileId, XML0_Base64Str1_Bytes2);
        }

        protected static void SerializeToFile(OrzEntity<T> obj, FileInfo fileId, int xml0_Base64Str1_Bytes2)
        {
            CreateDirectoryIfNotExists(fileId.Directory);
            if (0 == xml0_Base64Str1_Bytes2)
                File.WriteAllText(fileId.FullName, SerializationUtility.ToXML(obj));
            else if (1 == xml0_Base64Str1_Bytes2)
                File.WriteAllText(fileId.FullName, SerializationUtility.ToBas64String(obj));
            else
                File.WriteAllBytes(fileId.FullName, SerializationUtility.ToBytes(obj));

        }

        protected static T DeserializeFromFile(OrzEntity<T> obj, FileInfo fileId, int xml0_Base64Str1_Bytes2)
        {
            if (0 == xml0_Base64Str1_Bytes2)
                return (T)SerializationUtility.ToObject(File.ReadAllText(fileId.FullName), obj);
            else if (1 == xml0_Base64Str1_Bytes2)
                return (T)SerializationUtility.ToObject(File.ReadAllText(fileId.FullName));
            else
                return (T)SerializationUtility.ToObject(File.ReadAllBytes(fileId.FullName));
        }

        private static void ReflectionValueAssign(object a, object b)
        {
            if (ReflectionBaseTypeCompare(a.GetType(), b.GetType()))
            {
                foreach (var p in a.GetType().GetProperties())
                {
                    foreach (var p2 in b.GetType().GetProperties())
                    {
                        if (p.Name == p2.Name)
                        {
                            p.SetValue(a, p2.GetValue(b, null), null);
                        }
                    }
                }
            }
        }

        private static bool ReflectionBaseTypeCompare(Type t, Type t2)
        {
            if (t.FullName.StartsWith(t2.FullName)) return true;
            if (t == typeof(object)) return false;
            return ReflectionBaseTypeCompare(t.BaseType, t2);
        }

        private static void CreateDirectoryIfNotExists(DirectoryInfo dirInfo)
        {
            if (dirInfo == null) return;
            if (dirInfo.Parent != null) CreateDirectoryIfNotExists(dirInfo.Parent);
            if (!dirInfo.Exists) dirInfo.Create();
        }     
    }

    /// <summary>
    /// Utility class for serializing and deserializing objects
    /// </summary>

    public static class SerializationUtility
    {
        public static string ToXML(object value)
        {

            if (value == null)
            {
                return null;
            }
            string inMemoryBytes;
            using (var inMemoryData = new StringWriter())
            {
                new XmlSerializer(value.GetType()).Serialize(inMemoryData, value);

                inMemoryBytes = inMemoryData.ToString();
            }

            return inMemoryBytes;

        }

        public static byte[] ToBytes(object value)
        {
            if (value == null)
            {
                return null;
            }

            byte[] inMemoryBytes;
            using (var inMemoryData = new MemoryStream())
            {
                new BinaryFormatter().Serialize(inMemoryData, value);
                inMemoryBytes = inMemoryData.ToArray();
            }

            return inMemoryBytes;
        }

        public static string ToBas64String(object value)
        {
            return Convert.ToBase64String(ToBytes(value));
        }

        public static object ToObject(byte[] serializedObject)
        {
            if (serializedObject == null)
            {
                return null;
            }

            using (var dataInMemory = new MemoryStream(serializedObject))
            {
                return new BinaryFormatter().Deserialize(dataInMemory);
            }
        }

        public static object ToObject(string base64String)
        {
            return ToObject(Convert.FromBase64String(base64String));
        }

        public static object ToObject(string xml, object value)
        {
            if (xml == null || value == null)
            {
                return null;
            }

            using (var dataInMemory = new StringReader(xml))
            {
                return new XmlSerializer(value.GetType()).Deserialize(dataInMemory);

            }

        }

    }

}